"""
Grade Model Module
==================

This module implements the Grade class, which represents a student's grade for
an assignment or course within the GT-LMS system. It manages grade values, grading
criteria, and related information.

Main features:
- Grade information management
- Grade calculation and recording
- Grade history tracking
"""

from typing import Optional, Dict, Any
from datetime import datetime
from enum import Enum


class GradeType(Enum):
    """
    Enumeration of grade types
    """
    NUMERIC = "numeric"
    LETTER = "letter"
    PASS_FAIL = "pass_fail"
    PERCENTAGE = "percentage"


class LetterGrade(Enum):
    """
    Standard letter grade scale
    """
    A_PLUS = "A+"    # 97-100
    A = "A"          # 93-96
    A_MINUS = "A-"   # 90-92
    B_PLUS = "B+"    # 87-89
    B = "B"          # 83-86
    B_MINUS = "B-"   # 80-82
    C_PLUS = "C+"    # 77-79
    C = "C"          # 73-76
    C_MINUS = "C-"   # 70-72
    D_PLUS = "D+"    # 67-69
    D = "D"          # 65-66
    D_MINUS = "D-"   # 60-64
    F = "F"          # 0-59


class PassFailGrade(Enum):
    """
    Pass/Fail grade options
    """
    PASS = "Pass"
    FAIL = "Fail"
    INCOMPLETE = "Incomplete"


class Grade:
    """
    Grade - Represents a student's grade for an assignment or course
    
    This class contains all the information related to a student's grade,
    including the grade value, type, grader, feedback, and other relevant details.
    """
    
    def __init__(self, grade_id: str, student_id: str, assignment_id: str, 
                 grader_id: str, course_id: str, max_points: float = 100.0):
        """
        Initialize Grade instance
        
        Args:
            grade_id (str): Unique identifier for the grade
            student_id (str): ID of the student who received the grade
            assignment_id (str): ID of the assignment being graded
            grader_id (str): ID of the person who assigned the grade
            course_id (str): ID of the course this grade belongs to
            max_points (float): Maximum points possible for this assignment (default: 100.0)
        """
        self.grade_id = grade_id
        self.student_id = student_id
        self.assignment_id = assignment_id
        self.grader_id = grader_id
        self.course_id = course_id
        self.max_points = max_points
        self.grade_type: Optional[GradeType] = None
        self.numeric_grade: Optional[float] = None
        self.letter_grade: Optional[LetterGrade] = None
        self.pass_fail_grade: Optional[PassFailGrade] = None
        self.percentage_grade: Optional[float] = None
        self.feedback: str = ""
        self.comments: str = ""
        self.rubric_details: Dict[str, Any] = {}
        self.graded_at = datetime.now()
        self.last_updated = datetime.now()
        self.is_final = False
    
    def set_numeric_grade(self, points: float) -> None:
        """
        Set numeric grade value
        
        Args:
            points (float): Points earned
        """
        self.grade_type = GradeType.NUMERIC
        self.numeric_grade = points
        self.percentage_grade = (points / self.max_points) * 100 if self.max_points > 0 else 0
        self._update_letter_grade()
        self.last_updated = datetime.now()
    
    def set_letter_grade(self, letter: LetterGrade) -> None:
        """
        Set letter grade value
        
        Args:
            letter (LetterGrade): Letter grade
        """
        self.grade_type = GradeType.LETTER
        self.letter_grade = letter
        self._update_numeric_from_letter()
        self.last_updated = datetime.now()
    
    def set_pass_fail_grade(self, pass_fail: PassFailGrade) -> None:
        """
        Set pass/fail grade value
        
        Args:
            pass_fail (PassFailGrade): Pass/fail grade
        """
        self.grade_type = GradeType.PASS_FAIL
        self.pass_fail_grade = pass_fail
        self.last_updated = datetime.now()
    
    def set_percentage_grade(self, percentage: float) -> None:
        """
        Set percentage grade value
        
        Args:
            percentage (float): Percentage grade (0-100)
        """
        self.grade_type = GradeType.PERCENTAGE
        self.percentage_grade = percentage
        self.numeric_grade = (percentage / 100) * self.max_points
        self._update_letter_grade()
        self.last_updated = datetime.now()
    
    def _update_letter_grade(self) -> None:
        """
        Update letter grade based on numeric/percentage grade
        """
        if self.percentage_grade is not None:
            if self.percentage_grade >= 97:
                self.letter_grade = LetterGrade.A_PLUS
            elif self.percentage_grade >= 93:
                self.letter_grade = LetterGrade.A
            elif self.percentage_grade >= 90:
                self.letter_grade = LetterGrade.A_MINUS
            elif self.percentage_grade >= 87:
                self.letter_grade = LetterGrade.B_PLUS
            elif self.percentage_grade >= 83:
                self.letter_grade = LetterGrade.B
            elif self.percentage_grade >= 80:
                self.letter_grade = LetterGrade.B_MINUS
            elif self.percentage_grade >= 77:
                self.letter_grade = LetterGrade.C_PLUS
            elif self.percentage_grade >= 73:
                self.letter_grade = LetterGrade.C
            elif self.percentage_grade >= 70:
                self.letter_grade = LetterGrade.C_MINUS
            elif self.percentage_grade >= 67:
                self.letter_grade = LetterGrade.D_PLUS
            elif self.percentage_grade >= 65:
                self.letter_grade = LetterGrade.D
            elif self.percentage_grade >= 60:
                self.letter_grade = LetterGrade.D_MINUS
            else:
                self.letter_grade = LetterGrade.F
    
    def _update_numeric_from_letter(self) -> None:
        """
        Update numeric grade based on letter grade
        """
        if self.letter_grade is not None:
            # Approximate percentage based on letter grade
            letter_to_percentage = {
                LetterGrade.A_PLUS: 98.5,
                LetterGrade.A: 94.5,
                LetterGrade.A_MINUS: 91.0,
                LetterGrade.B_PLUS: 88.0,
                LetterGrade.B: 84.5,
                LetterGrade.B_MINUS: 81.0,
                LetterGrade.C_PLUS: 78.0,
                LetterGrade.C: 74.5,
                LetterGrade.C_MINUS: 71.0,
                LetterGrade.D_PLUS: 68.0,
                LetterGrade.D: 65.5,
                LetterGrade.D_MINUS: 62.0,
                LetterGrade.F: 30.0
            }
            
            self.percentage_grade = letter_to_percentage.get(self.letter_grade, 0)
            self.numeric_grade = (self.percentage_grade / 100) * self.max_points
    
    def add_feedback(self, feedback: str) -> None:
        """
        Add feedback to the grade
        
        Args:
            feedback (str): Feedback text
        """
        self.feedback = feedback
        self.last_updated = datetime.now()
    
    def add_comment(self, comment: str) -> None:
        """
        Add a comment to the grade
        
        Args:
            comment (str): Comment text
        """
        self.comments = comment
        self.last_updated = datetime.now()
    
    def add_rubric_detail(self, criterion: str, points: float, max_points: float, 
                         feedback: str = "") -> None:
        """
        Add rubric detail for a specific criterion
        
        Args:
            criterion (str): Rubric criterion name
            points (float): Points earned for this criterion
            max_points (float): Maximum points for this criterion
            feedback (str): Feedback for this criterion
        """
        self.rubric_details[criterion] = {
            'points': points,
            'max_points': max_points,
            'feedback': feedback
        }
        self.last_updated = datetime.now()
    
    def get_total_rubric_points(self) -> float:
        """
        Calculate total points from rubric details
        
        Returns:
            float: Total points from rubric
        """
        return sum(detail['points'] for detail in self.rubric_details.values())
    
    def get_max_rubric_points(self) -> float:
        """
        Calculate maximum possible points from rubric details
        
        Returns:
            float: Maximum possible points from rubric
        """
        return sum(detail['max_points'] for detail in self.rubric_details.values())
    
    def finalize_grade(self) -> None:
        """
        Mark the grade as final
        """
        self.is_final = True
        self.last_updated = datetime.now()
    
    def is_passing(self) -> bool:
        """
        Check if the grade is passing
        
        Returns:
            bool: True if grade is passing, False otherwise
        """
        if self.grade_type == GradeType.PASS_FAIL:
            return self.pass_fail_grade == PassFailGrade.PASS
        elif self.percentage_grade is not None:
            return self.percentage_grade >= 60  # Assuming 60% is passing
        elif self.letter_grade is not None:
            return self.letter_grade not in [LetterGrade.F]
        return False
    
    def get_grade_display(self) -> str:
        """
        Get a display string for the grade
        
        Returns:
            str: Formatted grade string
        """
        if self.grade_type == GradeType.NUMERIC:
            return f"{self.numeric_grade}/{self.max_points}"
        elif self.grade_type == GradeType.LETTER:
            return self.letter_grade.value if self.letter_grade else "N/A"
        elif self.grade_type == GradeType.PASS_FAIL:
            return self.pass_fail_grade.value if self.pass_fail_grade else "N/A"
        elif self.grade_type == GradeType.PERCENTAGE:
            return f"{self.percentage_grade:.1f}%"
        else:
            return "No Grade"
    
    def to_dict(self) -> dict:
        """
        Convert grade object to dictionary for serialization
        
        Returns:
            dict: Dictionary representation of the grade
        """
        return {
            'grade_id': self.grade_id,
            'student_id': self.student_id,
            'assignment_id': self.assignment_id,
            'grader_id': self.grader_id,
            'course_id': self.course_id,
            'max_points': self.max_points,
            'grade_type': self.grade_type.value if self.grade_type else None,
            'numeric_grade': self.numeric_grade,
            'letter_grade': self.letter_grade.value if self.letter_grade else None,
            'pass_fail_grade': self.pass_fail_grade.value if self.pass_fail_grade else None,
            'percentage_grade': self.percentage_grade,
            'feedback': self.feedback,
            'comments': self.comments,
            'rubric_details': self.rubric_details,
            'graded_at': self.graded_at.isoformat(),
            'last_updated': self.last_updated.isoformat(),
            'is_final': self.is_final
        }
    
    @classmethod
    def from_dict(cls, data: dict) -> 'Grade':
        """
        Create Grade instance from dictionary
        
        Args:
            data (dict): Dictionary containing grade data
            
        Returns:
            Grade: New Grade instance
        """
        grade = cls(
            data['grade_id'],
            data['student_id'],
            data['assignment_id'],
            data['grader_id'],
            data['course_id'],
            data['max_points']
        )
        
        # Set grade type and values
        if data.get('grade_type'):
            grade.grade_type = GradeType(data['grade_type'])
        
        grade.numeric_grade = data.get('numeric_grade')
        grade.percentage_grade = data.get('percentage_grade')
        grade.feedback = data.get('feedback', '')
        grade.comments = data.get('comments', '')
        grade.rubric_details = data.get('rubric_details', {})
        grade.is_final = data.get('is_final', False)
        
        # Handle enum values
        if data.get('letter_grade'):
            grade.letter_grade = LetterGrade(data['letter_grade'])
        
        if data.get('pass_fail_grade'):
            grade.pass_fail_grade = PassFailGrade(data['pass_fail_grade'])
        
        # Parse datetime strings
        grade.graded_at = datetime.fromisoformat(data['graded_at'])
        grade.last_updated = datetime.fromisoformat(data['last_updated'])
        
        return grade
